package com.github.kongchen.swagger.docgen.reader;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseStatus;

import com.github.kongchen.swagger.docgen.GenerateException;
import com.github.kongchen.swagger.docgen.LogAdapter;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.Authorization;
import io.swagger.annotations.AuthorizationScope;
import io.swagger.converter.ModelConverters;
import io.swagger.models.Model;
import io.swagger.models.Operation;
import io.swagger.models.Response;
import io.swagger.models.SecurityRequirement;
import io.swagger.models.Swagger;
import io.swagger.models.Tag;
import io.swagger.models.parameters.BodyParameter;
import io.swagger.models.parameters.Parameter;
import io.swagger.models.properties.ArrayProperty;
import io.swagger.models.properties.MapProperty;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.PropertyBuilder;
import io.swagger.models.properties.RefProperty;

public class PSApiReader extends AbstractReader {

	public PSApiReader(Swagger swagger, LogAdapter LOG) {
		super(swagger, LOG);
	}

    @Override
    public Swagger read(Set<Class<?>> classes) throws GenerateException {
        for (Class<?> clazz : classes) {
            read(clazz);
        }

        return swagger;
    }

    public Swagger read(Class<?> clazz) 
    {
    	if (swagger == null) {
            swagger = new Swagger();
        }
    	
        Method[] methods = clazz.getMethods();
        Map<String, Tag> tags = new HashMap<String, Tag>();

        List<SecurityRequirement> resourceSecurities = new ArrayList<SecurityRequirement>();

        if (clazz.isAnnotationPresent(Api.class)) {
            Api api = AnnotationUtils.findAnnotation(clazz, Api.class);
            if (!canReadApi(false, api)) {
                return swagger;
            }
            tags = updateTagsForApi(getDefaultTags(clazz), api);
            resourceSecurities = getSecurityRequirements(api);
        }

        //collect api from method with @RequestMapping
        for(Method method:methods)
        {	
        	ApiOperation apiOperation = AnnotationUtils.findAnnotation(method, ApiOperation.class);
        	if (apiOperation == null || apiOperation.hidden()) 
        		continue;

            String httpMethod ="post";
            Operation operation = parseMethod(method);
            if(operation==null)
            	continue;
            updateOperationParameters(new ArrayList<Parameter>(),new HashMap<String, String>(),operation);
            updateOperationProtocols(apiOperation, operation);
            updateTagsForOperation(operation, apiOperation);
            updateOperation(new String[]{}, new String[]{}, tags, resourceSecurities, operation);
            updatePath(resolvPath(method), httpMethod, operation);
        }            
        return swagger;
    }

    private String resolvPath(Method method)
    {
    	String methodName=method.getName();
    	Class<?>[] paramTypes=method.getParameterTypes();
    	ParameterNameDiscoverer paramNameDiscover=new LocalVariableTableParameterNameDiscoverer();
        String[] paramNames=paramNameDiscover.getParameterNames(method);
    	if(paramTypes!=null&&paramTypes.length>0)
    	{
    		StringBuilder paramStr=new StringBuilder();
    		for(int i=0;i<paramNames.length;i++)
    		{
    			paramStr.append(",").append(paramTypes[i].getSimpleName())
    				.append(" ").append(paramNames[i]);
    		}
    		methodName=methodName+"("+paramStr.substring(1)+")";
    	}
    	else
    		methodName+="()";
    	return methodName+=":"+method.getReturnType().getSimpleName();
    }
    
    private Operation parseMethod(Method method) {
        Operation operation = new Operation();

	    Type responseClass = null;
        String responseContainer = null;
        String operationId = method.getName();

        ApiOperation apiOperation = AnnotationUtils.findAnnotation(method, ApiOperation.class);

        if (apiOperation.hidden()) {
            return null;
        }
        if (!apiOperation.nickname().isEmpty()) {
            operationId = apiOperation.nickname();
        }

        Map<String, Property> defaultResponseHeaders = parseResponseHeaders(apiOperation.responseHeaders());

        operation.summary(apiOperation.value()).description(apiOperation.notes());

        Set<Map<String, Object>> customExtensions = parseCustomExtensions(apiOperation.extensions());

        for (Map<String, Object> extension : customExtensions) {
            if (extension == null) {
                continue;
            }
            for (Map.Entry<String, Object> map : extension.entrySet()) {
                operation.setVendorExtension(
                        map.getKey().startsWith("x-")
                                ? map.getKey()
                                : "x-" + map.getKey(), map.getValue()
                );
            }
        }

        if (!apiOperation.response().equals(Void.class)) {
            responseClass = apiOperation.response();
        }
        if (!apiOperation.responseContainer().isEmpty()) {
            responseContainer = apiOperation.responseContainer();
        }

        ///security
        List<SecurityRequirement> securities = new ArrayList<SecurityRequirement>();
        for (Authorization auth : apiOperation.authorizations()) {
            if (!auth.value().isEmpty()) {
                SecurityRequirement security = new SecurityRequirement();
                security.setName(auth.value());
                for (AuthorizationScope scope : auth.scopes()) {
                    if (!scope.scope().isEmpty()) {
                        security.addScope(scope.scope());
                    }
                }
                securities.add(security);
            }
        }
        for (SecurityRequirement sec : securities) {
            operation.security(sec);
        }

        if (responseClass == null) {
            // pick out response from method declaration
            LOG.info("picking up response class from method " + method);
            responseClass = method.getGenericReturnType();
        }
        if (responseClass instanceof ParameterizedType && ResponseEntity.class.equals(((ParameterizedType) responseClass).getRawType())) {
            responseClass = ((ParameterizedType) responseClass).getActualTypeArguments()[0];
        }
        boolean hasApiAnnotation = false;
        if (responseClass instanceof Class) {
            hasApiAnnotation = AnnotationUtils.findAnnotation((Class) responseClass, Api.class) != null;
        }
        if (responseClass != null
                && !responseClass.equals(Void.class)
                && !responseClass.equals(ResponseEntity.class)
                && !hasApiAnnotation) {
            if (isPrimitive(responseClass)) {
                Property responseProperty;
                Property property = ModelConverters.getInstance().readAsProperty(responseClass);
                if (property != null) {
                    if ("list".equalsIgnoreCase(responseContainer)) {
                        responseProperty = new ArrayProperty(property);
                    } else if ("map".equalsIgnoreCase(responseContainer)) {
                        responseProperty = new MapProperty(property);
                    } else {
                        responseProperty = property;
                    }
                    operation.response(apiOperation.code(), new Response()
                            .description("successful operation")
                            .schema(responseProperty)
                            .headers(defaultResponseHeaders));
                }
            } else if (!responseClass.equals(Void.class) && !responseClass.equals(void.class)) {
                Map<String, Model> models = ModelConverters.getInstance().read(responseClass);
                if (models.isEmpty()) {
                    Property pp = ModelConverters.getInstance().readAsProperty(responseClass);
                    operation.response(apiOperation.code(), new Response()
                            .description("successful operation")
                            .schema(pp)
                            .headers(defaultResponseHeaders));
                }
                for (String key : models.keySet()) {
                    Property responseProperty;

                    if ("list".equalsIgnoreCase(responseContainer)) {
                        responseProperty = new ArrayProperty(new RefProperty().asDefault(key));
                    } else if ("map".equalsIgnoreCase(responseContainer)) {
                        responseProperty = new MapProperty(new RefProperty().asDefault(key));
                    } else {
                        responseProperty = new RefProperty().asDefault(key);
                    }
                    operation.response(apiOperation.code(), new Response()
                            .description("successful operation")
                            .schema(responseProperty)
                            .headers(defaultResponseHeaders));
                    swagger.model(key, models.get(key));
                }
                models = ModelConverters.getInstance().readAll(responseClass);
                for (Map.Entry<String, Model> entry : models.entrySet()) {
                    swagger.model(entry.getKey(), entry.getValue());
                }
            }
        }

        operation.operationId(operationId);

        ApiResponses responseAnnotation = AnnotationUtils.findAnnotation(method, ApiResponses.class);
        if (responseAnnotation != null) {
            updateApiResponse(operation, responseAnnotation);
        } else {
            ResponseStatus responseStatus = AnnotationUtils.findAnnotation(method, ResponseStatus.class);
            if (responseStatus != null) {
                operation.response(responseStatus.value().value(), new Response().description(responseStatus.reason()));
            }
        }

        Deprecated annotation = AnnotationUtils.findAnnotation(method, Deprecated.class);
        if (annotation != null) {
            operation.deprecated(true);
        }

        // process parameters
        Class[] parameterTypes = method.getParameterTypes();
        Type[] genericParameterTypes = method.getGenericParameterTypes();
        Annotation[][] paramAnnotations = method.getParameterAnnotations();
        // paramTypes = method.getParameterTypes
        // genericParamTypes = method.getGenericParameterTypes
        ParameterNameDiscoverer paramNameDiscover=new LocalVariableTableParameterNameDiscoverer();
        String[] paramNames=paramNameDiscover.getParameterNames(method);
        String paramName;
        for (int i = 0; i < parameterTypes.length; i++) 
        {
        	Type type = genericParameterTypes[i];
            List<Annotation> annotations = Arrays.asList(paramAnnotations[i]);
            List<Parameter> parameters = getParameters(type, annotations);
            paramName=paramNames[i];
            if(parameters.size()==0)
            {
            	parameters=new ArrayList<Parameter>();
            	BodyParameter temp=new BodyParameter();
            	parameters.add(temp);
            	temp.setSchema(PropertyBuilder.toModel(ModelConverters.getInstance().readAsProperty(type)));
            	if(isNullObject(temp))
            		temp.setSchema(null);
            	else
            	{
            		for (Map.Entry<String, Model> entry : ModelConverters.getInstance().readAll(type).entrySet()) 
            			swagger.addDefinition(entry.getKey(), entry.getValue());
                }
            }
            for (Parameter parameter : parameters) 
            {
            	if(parameter.getName()==null||parameter.getName().equals("body"))
            		parameter.setName(paramName);
            	if(StringUtils.isEmpty(parameter.getDescription()))
            		parameter.setDescription(parameterTypes[i].getName());
            	operation.parameter(parameter);
            }
        }
       
        if (operation.getResponses() == null) {
            operation.defaultResponse(new Response().description("successful operation"));
        }

        // Process @ApiImplicitParams
        this.readImplicitParameters(method, operation);

        return operation;
    }
    
}
